package hips;

import hips.region.NewRegionEvent;
import hips.region.NewRegionListener;
import hips.tools.WorkingData;

import javax.swing.event.EventListenerList;

/**
 * Esta clase almacena la partición de una imagen creada a partir del algoritmo
 * de segmentación utilizado en la libreria HIPS. Este algoritmo estó basado en
 * la conectividad reestringida por parámetros. Los parámetros utilizados son:
 * el parámetro de rango local, el parámetro de rango global, y el índice de
 * conectividad.
 */
public abstract class Partition<PixelValue_ extends PixelValue<PixelValue_>> {

	protected Image<PixelValue_> input;
	protected int[] references;
	protected int[] pixels;
	protected int[] lbl;
	protected EventListenerList listenerList;
	private PixelValue_ alpha;
	private PixelValue_ omega;
	protected float cindex;
	protected int randomPixels;
	protected int method;

	/**
	 * Identifica a las clases hijas de <i>Partition</i> que utilizan un método
	 * de segmentación que sigue las pautas marcadas en el algoritmo propuesto
	 * por Pierre Soille en su artículo "Constrained Connectivity for
	 * Hierarchical Image Partitioning and Simplification"
	 */
	public static final int ORIGINAL = 0;

	/**
	 * Identifica a las clases hijas de <i>Partition</i> que utilizan un método
	 * de segmentación con mejoras con respecto al propuesto por Pierre Soille
	 * en su artículo "Constrained Connectivity for Hierarchical Image
	 * Partitioning and Simplification"
	 */
	public static final int IMPROVED_WITHOUT_CINDEX = 1;

	/**
	 * Identifica a las clases hijas de <i>Partition</i> que utilizan un método
	 * de segmentación con mejoras con respecto al propuesto por Pierre Soille
	 * en su artículo "Constrained Connectivity for Hierarchical Image
	 * Partitioning and Simplification", y que además introducen un tercer
	 * parámetro: el índice de conectividad.
	 */
	public static final int IMPROVED_WITH_CINDEX = 2;

	/**
	 * Almacena un valor adecuado para el número de píxeles que se deben de
	 * visitar de forma aleatoria al principio del método <i>makeRegions</i>.
	 */
	public static final int RANDOM = 500;

	/**
	 * Método que ejecuta el algoritmo de segmentación y crea las regiones
	 * correspondientes a la partición generada.
	 */
	public abstract void makeRegions();

	/**
	 * Inicializa una instancia de la clase, indicando la imagen de entrada y
	 * los parámetros que se van a utilizar para la segmentación.
	 * 
	 * @param input
	 *            Imagen que va a ser segmentada.
	 * @param alpha
	 *            Parámetro de rango local.
	 * @param omega
	 *            Parámetro de rango global.
	 * @param cindex
	 *            Índice de conectividad.
	 * @param randomPixels
	 *            Establece el número de píxeles que se examinarán de manera
	 *            aleatoria al comienzo de la llamada al método
	 *            <i>makeRegions</i>.
	 */
	public Partition(Image<PixelValue_> input, PixelValue_ alpha, PixelValue_ omega, float cindex, int randomPixels) {
		this.input = input;
		this.alpha = alpha;
		this.omega = omega;
		this.cindex = cindex;
		this.randomPixels = randomPixels;
		references = new int[0];
		pixels = null;
		lbl = null;
		listenerList = new EventListenerList();
	}

	/**
	 * Inicializa una instancia de la clase, indicando la imagen de entrada. A
	 * la hora de segmentar la imagen mediante la llamada a la función
	 * makeRegions, se utilizarán unos parámetros predefinidos.
	 * 
	 * @param input
	 *            Imagen que va a ser segmentada.
	 */

	public Partition(Image<PixelValue_> input) {
		this.input = input;
		omega = getDefaultOmega(input);
		alpha = input.newPixelValue(getDefaultAlpha(input));
		cindex = 0.0f;
		randomPixels = 0;
		references = new int[0];
		pixels = null;
		lbl = null;
		listenerList = new EventListenerList();
	}

	/**
	 * Obtiene un parámetro de rango global adecuado para la imagen <i>img</i>.
	 */
	public static <P extends PixelValue<P>> P getDefaultOmega(Image<P> img) {
		return img.getMaxRange().div(1.5f);
	}

	/**
	 * Obtiene un parámetro de rango local adecuado para la imagen <i>img</i>.
	 */
	public static <P extends PixelValue<P>> Number getDefaultAlpha(Image<P> img) {
		PixelValue<P> value = img.getMaxRange();
		value.div(2f);
		value.div(img.getChannels() * img.getSlices());
		return value.sum();
	}

	/**
	 * Obtiene la región a la que pertenece el píxel.
	 * 
	 * @param pixel
	 *            Posición del pixel dentro de la imagen. Se calcula como: <i>x
	 *            * ancho + y</i>.
	 */
	public Region getRegionByPixel(int pixel) {
		int regionIndex = lbl[pixel];
		int base = references[regionIndex];
		int size = 0;
		if (regionIndex < getRegionSize() - 1) {
			size = references[regionIndex + 1] - references[regionIndex];
		} else {
			size = getImage().getSize() - references[regionIndex];
		}
		return new Region(pixels, base, size, regionIndex);
	}

	/**
	 * Obtiene la región a partir de su etiqueta.
	 * 
	 * @param label
	 *            Etiqueta que le corresponde a la región en la partición. Las
	 *            etiquetas de una partición toman sus valores desde <i>cero</i>
	 *            hasta <i>número de regiones - 1</i>.
	 */
	public Region getRegionByLabel(int label) {
		int base = references[label];
		int size;
		if (label < references.length - 1) {
			size = references[label + 1] - references[label];
		} else {
			size = input.getSize() - references[label];
		}
		return new Region(pixels, base, size, label);
	}

	/**
	 * Comprueba si la partición es jerárquicamente inferior a la partición que
	 * se le pasa por parámetro. Si esto ocurre, cada una de las regiones que la
	 * conforma están contenidas dentro de una región de la otra partición. La
	 * partición será jerárquicamente inferior si se cumplen las siguientes
	 * condiciones:
	 * <p>
	 * - el parámetro de rango local es menor o igual en todos los canales de la
	 * imagen. <br>
	 * - el parámetro de rango global es menor o igual en todos los canales de
	 * la imagen. <br>
	 * - el índice de conectividad es mayor o igual.
	 * 
	 * @return Devuelve <i>true</i> si la partición es jerárquicamente inferior,
	 *         y <i>false</i> en caso contrario.
	 */
	public boolean hierarchycallyLowerThan(Partition<PixelValue_> other) {
		if (!alpha.isLowerOrEqual(other.alpha)) {
			return false;
		}
		if (!omega.isLowerOrEqual(other.omega)) {
			return false;
		}
		if (cindex < other.cindex) {
			return false;
		}
		if (alpha.isEqual(other.alpha) && omega.isEqual(other.omega) && cindex == other.cindex) {
			return false;
		}
		return true;
	}

	/**
	 * Comprueba si la partición es jerárquicamente superior a la partición que
	 * se le pasa por parámetro. La partición será jerárquicamente superior si
	 * se cumplen las siguientes condiciones:
	 * <p>
	 * - el parámetro de rango local es mayor o igual en todos los canales de la
	 * imagen. <br>
	 * - el parámetro de rango global es mayor o igual en todos los canales de
	 * la imagen. <br>
	 * - el índice de conectividad es menor o igual.
	 * 
	 * @return Devuelve <i>true</i> si la partición es jerárquicamente superior,
	 *         y <i>false</i> en caso contrario.
	 */
	public boolean hierarchycallyGreaterThan(Partition<PixelValue_> other) {
		if (!alpha.isGreaterOrEqual(other.alpha)) {
			return false;
		}
		if (!omega.isGreaterOrEqual(other.omega)) {
			return false;
		}
		if (cindex > other.cindex) {
			return false;
		}
		if (alpha.isEqual(other.alpha) && omega.isEqual(other.omega) && cindex == other.cindex) {
			return false;
		}
		return true;
	}

	/**
	 * Comprueba si la partición es jerárquicamente del mismo orden que la
	 * partición que se le pasa por parámetro. Las particines serán
	 * jerárquicamente iguales si se cumplen las siguientes condiciones:
	 * <p>
	 * - los parámetros de rango local son iguales en todos los canales. <br>
	 * - los parámetros de rango global son iguales en todos los canales. <br>
	 * - los índices de conectividad son iguales.
	 * 
	 * @return Devuelve <i>true</i> si las particiones son jerárquicamente
	 *         iguales, y <i>false</i> en caso contrario.
	 */
	public boolean hierarchycallyEqualTo(Partition<PixelValue_> other) {
		if (alpha.isEqual(other.alpha) && omega.isEqual(other.omega) && cindex == other.cindex) {
			return true;
		}
		return false;
	}

	/**
	 * Obtiene la imagen de entrada.
	 */
	public Image<PixelValue_> getImage() {
		return input;
	}

	/**
	 * Establece la imagen de entrada.
	 */
	public void setImage(Image<PixelValue_> input) {
		this.input = input;
	}

	/**
	 * Obtiene el parámetro de rango local.
	 */
	public PixelValue_ getAlpha() {
		return alpha;
	}

	/**
	 * Obtiene el parámetro de rango global.
	 */
	public PixelValue_ getOmega() {
		return omega;
	}

	/**
	 * Obtiene el índice de conectividad.
	 */
	public float getCindex() {
		return cindex;
	}

	/**
	 * Establece el parámetro de rango local.
	 */
	public void setAlpha(PixelValue_ alpha) {
		if (lbl == null) {
			this.alpha = alpha;
		}
	}

	/**
	 * Establece el parámetro de rango global.
	 */
	public void setOmega(PixelValue_ omega) {
		if (lbl == null) {
			this.omega = omega;
		}
	}

	/**
	 * Asigna el índice de conectividad.
	 */
	public void setCindex(float cindex) {
		if (lbl == null) {
			this.cindex = cindex;
		}
	}

	/**
	 * Obtiene el número de píxeles aleatorios que se visitan.
	 */
	public int getRandomPixels() {
		return randomPixels;
	}

	/**
	 * Establece el número de píxeles aleatorios que se visitan.
	 */
	public void setRandomPixels(int randomPixels) {
		this.randomPixels = randomPixels;
	}

	/**
	 * Obtiene el número de regiones de la partición.
	 */
	public int getRegionSize() {
		return references.length;
	}

	/**
	 * Obtiene el método de segmentación utilizado. Puede valer:
	 * <i>ORIGINAL</i>, <i>IMPROVED_WITHOUT_CINDEX</i>, o
	 * <i>IMPROVED_WITH_CINDEX</i>.
	 */
	public int getMethod() {
		return method;
	}

	/**
	 * Transforma la imagen y los parámetros parámetros alpha y omega, cuando el
	 * vector alpha no contiene los mismos valores en todos los canales.
	 * 
	 * @return Devuelve los objetos transformados.
	 */
	protected WorkingData<PixelValue_> getWorkingData() {
		if (alpha.maxValue().floatValue() - alpha.minValue().floatValue() == 0) {
			return new WorkingData<PixelValue_>(input, alpha.maxValue(), omega);
		}
		float workingAlpha = alpha.maxValue().floatValue();
		float[][] transforming = new float[input.getSlices()][input.getChannels()];
		PixelValue_ newOmega = input.newPixelValue();
		for (int i = 0; i < input.getSlices(); i++) {
			for (int j = 0; j < input.getChannels(); j++) {
				transforming[i][j] = workingAlpha / alpha.getValue(i, j).floatValue();
				newOmega.setValue(omega.getValue(i, j).floatValue() * transforming[i][j], i, j);
			}
		}
		return new WorkingData<PixelValue_>(getWorkingImage(transforming), alpha.maxValue(), newOmega);
	}

	private Image<PixelValue_> getWorkingImage(float[][] transforming) {
		Image<PixelValue_> working = input.newImage();
		PixelValue_ p = input.newPixelValue();
		for (int k = 0; k < input.getSize(); k++) {
			input.getPixelValue(p, k);
			for (int i = 0; i < input.getSlices(); i++) {
				for (int j = 0; j < input.getChannels(); j++) {
					p.setValue(p.getValue(i, j).floatValue() * transforming[i][j], i, j);
				}
			}
			working.putPixelValue(k, p);
		}
		return working;
	}

	/**
	 * añade un listener que escuchará los eventos relacionados con las nuevas
	 * regiones creadas, cuando se llame al método makeRegions.
	 */
	public void addNewRegionEventListener(NewRegionListener listener) {
		listenerList.add(NewRegionListener.class, listener);
	}

	/**
	 * borra el listener si fué añadido anteriormente.
	 */
	public void removeNewRegionEventListener(NewRegionListener listener) {
		listenerList.remove(NewRegionListener.class, listener);
	}

	protected void fireRegionEvent(NewRegionEvent evt) {
		Object[] listeners = listenerList.getListenerList();
		for (int i = 0; i < listeners.length; i += 2) {
			if (listeners[i] == NewRegionListener.class) {
				((NewRegionListener) listeners[i + 1]).newRegionCreated(evt);
			}
		}
	}
	
	public int[] getLbl(){
		return lbl;
	}
}
